Flutter UI渲染分析
1、前言
本篇文章主要介绍Flutter 渲染框架及其渲染过程
Flutter是谷歌的移动UI框架,在此之前也有类似ReactNative、Weex等跨端方案,Flutter在一定程度上借鉴了ReactNative的思想,采用三棵树 其中element tree diff管理,来触发renderTree的刷新,并且不同于android这种命令式视图开发,采用了声明式,下面将一一介绍。
2、编程范式的改变
在Android视图开发中是命令式的,view大多数都是在xml声明,开发者然后通过id找出view,数据更新时,仍需要开发者关注需要变化的view,再调用方法比如 setText之类的使其发生改变;
但是在Flutter中视图的开发是声明式的,开发者需要维护好一套数据集合以及绑定好widgetTree,这样后面数据变化时候widget会根据数据来渲染,开发者就不再关注每个组件,关心核心数据即可。
3、Flutter 渲染框架介绍
Flutter的渲染框架分为Framework和Engine两层,应用是基于Framework层开发,其中
- Framework层负责渲染中的Build、Layout、Paint、生成Layer等环节,使用Dart语言
- Engine层是C++实现的渲染引擎,负责把Framework生成的Layer组合,生成纹理,然后通过OpenGL接口向GPU提交渲染数据
该跨平台应用框架没有使用webview或者平台自带的组件,使用自身的高性能渲染引擎Skia 自绘,组件之间可以任意组合
4、视图树
flutter中通过各种各样的widget组合使用,视图树中包含了以下三种树 Widget、Element、RenderObject,对应关系如下
- Widget:存放渲染内容、视图布局信息,widget的属性最好都是immutable
- Element:存放上下文,通过Element遍历视图树,Element同时持有Widget和RenderObject(BuilderOwner)
- RenderObject:根据Widget的布局属性进行layout,paint Widget传人的内容(PipeLineOwner)
通常 我们创建widget树,然后调用runApp(rootWidget),将rootWidget传给rootElement,作为rootElement的子节点,生成Element树,由Element树生成Render树
widget是immutable,数据变化会重绘,如何避免资源消耗
Flutter界面开发是一种响应式的编程,当数据发生变化时通知到可变更的节点(statefullWidget或者rootwidget),但是每次数据变更,都会触发widgetTree的重绘,由于widget只是持有一些渲染的配置信息而已,不是真正触发渲染的对象,非常轻量级,flutter团队对widget的创建、销毁做了优化,不用担心整个widget树重新创建带来的性能问题。RenderObject才是真正渲染时使用,涉及到layout、paint等复杂操作,是一个真正渲染的view,二者被Element Tree持有,ElementTree通过Diff 算法来将不断变化的widget转变为相对稳定的RenderObject。
当我们不断改变widget时,BuilderOwner收到widgetTree会与之前的widgetTree作对比,在ElementTree上只更新变化的部分,当Elment变化之后 与之对应的RenderObject也就更新了,如下图所示
可以看到WidgetTree全部被替换了,但是ElmentTree和RenderObjectTree只替换了变化的部分
其中 PipelineOwner类似于Android中的ViewRootImpl,管理着真正需要绘制的View,
最后PipelineOwner会对RenderObjectTree中发生变化节点的进行layout、paint、合成等等操作,最后交给底层引擎渲染。
Widget、Element、RenderObject之间的关系
在介绍Elment Tree的Diff规则之前,先介绍下,这三者之前的关系,之前也大致提到 Elment Tree持有了Element同时持有Widget和RenderObject(BuilderOwner),我们先从代码入手
可以看出 Widget抽象类有3个关键能力
- 保证自身唯一性的key
- 创建Element的create
- canUpdate
从上面类图也可以看出,Element和RenderObject都是由Widget创建出来,也并不是每一个Widget都有与之对应的RenderObject
Widget、Element、RenderObject 的第一次创建与关联
在Android中ViewTree1
2
3
4-PhoneWindow
- DecorView
- TitleView
- ContentView
而在Flutter中则比较简单,只有底层的root widget1
2
3- RenderObjectToWidgetAdapter<RenderBox>
- MyApp (自定义)
- MyMaterialApp (自定义)
其中RenderObjectToWidgetAdapter 也是一个renderObjectWidget,通过注释可以发现它是runApp启动时“A bridge from a [RenderObject] to an [Element] tree.”
runApp代码
1 | void runApp(Widget app) { |
WidgetsFlutterBinding 初始化了一系列的Binding,这些Binding持有了我们上面说的一些owner,比如BuildOwner,PipelineOwner,所以随着WidgetsFlutterBinding的初始化,其他的Binding也被初始化了,
GestureBinding | 提供了 window.onPointerDataPacket 回调,绑定 Framework 手势子系统,是 Framework 事件模型与底层事件的绑定入口 |
---|---|
ServicesBinding | 提供了 window.onPlatformMessage 回调, 用于绑定平台消息通道(message channel),主要处理原生和 Flutter 通信 |
SchedulerBinding | 提供了 window.onBeginFrame 和 window.onDrawFrame 回调,监听刷新事件,绑定 Framework 绘制调度子系统 |
PaintingBinding | 绑定绘制库,主要用于处理图片缓存 |
SemanticsBinding | 语义化层与 Flutter engine 的桥梁,主要是辅助功能的底层支持 |
RendererBinding | 提供了 window.onMetricsChanged 、window.onTextScaleFactorChanged 等回调。它是渲染树与 Flutter engine 的桥梁 |
WidgetsBinding | 提供了 window.onLocaleChanged、onBuildScheduled 等回调。它是 Flutter widget 层与 engine 的桥梁 |
继续跟进下attachRootWidget(app)
1 | void attachRootWidget(Widget rootWidget) { |
内部创建了 RenderObjectToWidgetAdapter 并将我们传入的app 自定义widget做了child,接着执行attachToRenderTree这个方法,创建了第一个Element和RenderObject
1 | RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) { |
我们解释一下上面的图片,Root的创建比较简单:
- 1.
attachRootWidget(app)
方法创建了Root[Widget](也就是 RenderObjectToWidgetAdapter) - 2.紧接着调用
attachToRenderTree
方法创建了 Root[Element] - 3.Root[Element]尝试调用
mount
方法将自己挂载到父Element上,因为自己就是root了,所以没有父Element,挂空了 - 4.mount的过程中会调用Widget的
createRenderObject
,创建了 Root[RenderObject]
它的child,也就是我们传入的app是怎么挂载父控件上的呢?
- 5.我们将app作为Root[Widget](也就是 RenderObjectToWidgetAdapter),appp[Widget]也就成了为root[Widget]的child[Widget]
- 6.调用
owner.buildScope
,开始执行子Tree的创建以及挂载,敲黑板!!!这中间的流程和WidgetTree的刷新流程是一模一样的,详细流程我们后面讲! - 7.调用
createElement
方法创建出Child[Element] - 8.调用Element的
mount
方法,将自己挂载到Root[Element]上,形成一棵树 - 9.挂载的同时,调用
widget.createRenderObject
,创建Child[RenderObject] - 10.创建完成后,调用
attachRenderObject
,完成和Root[RenderObject]的链接
就这样,WidgetTree、ElementTree、RenderObject创建完成,并有各自的链接关系。
这里有两个操作需要注意下,
mount
1 | abstract class Element: |
我们先看一下Element的挂载,就是让_parent
持有父Element的引用,因为RootElement 是没有父Element的,所以参数传了null:element.mount(null, null);
还有两个值得注意的地方:
- 节点的深度_depth 也是在这个时候计算的,深度对刷新很重要
- 每个Element的buildOwner,都来自父类的BuildOwner,这样可以保证一个ElementTree,只由一个BuildOwner来维护。
RenderObjectElement
1 | abstract class RenderObjectElement: |
RenderObject与父RenderObject的挂载稍微复杂了点。通过代码我们可以看到需要先查询一下自己的AncestorRenderObject
,这是为什么呢?
还记得之前我们讲过,每一个Widget都有一个对应的Element,但Element不一定会有对应的RenderObject。所以你的父Element并不一有RenderObject,这个时候就需要向上查找。
1 | RenderObjectElement _findAncestorRenderObjectElement() { |
通过代码我们也可以看到,find方法在向上遍历Element,直到找到RenderObjectElement,RenderObjectElement肯定是有对应的RenderObject了,这个时候在进行RenderObject子父间的挂载。
5、渲染过程
当需要更新UI的时候,Framework通知Engine,Engine会等到下个Vsync信号到达的时候,会通知Framework,然后Framework会进行animations,
build,layout,compositing,paint,最后生成layer提交给Engine。Engine会把layer进行组合,生成纹理,最后通过Open Gl接口提交数据给GPU,
GPU经过处理后在显示器上面显示。整个流程如下图:
6、渲染触发 (setState)
setState背后发生了什么
在Flutter开发应用的时候,当需要更新的UI的时候,需要调用一下setState方法,然后就可以实现了UI的更新,我们接下来分析一下该方法做哪些事情。
1 | void setState(VoidCallback fn) { |
继续追踪
1 | void markNeedsBuild() { |
widget对应的element将自身标记为dirty
状态,并调用owner.scheduleBuildFor(this);
通知buildOwner进行处理
1 | void scheduleBuildFor(Element element) { |
后续MyStatefulWidget的build方法一定会被执行,执行后,会创建新的子Widget出来,原来的子Widget便被抛弃掉了,原来的子Widget肯定是没救了,但他们的Element大概率还是有救的,此时 buildOwner会将所有dirty的Element添加到_dirtyElements当中
经过Framework一连串的调用后,最终调用scheduleFrame来通知Engine需要更新UI,Engine就会在下个vSync到达的时候通过调用_drawFrame来通知Framework,然后Framework就会通过BuildOwner进行Build和PipelineOwner进行Layout,Paint,最后把生成Layer,组合成Scene提交给Engine。
底层引擎最终回到Dart层,并执行buildOwner的buildScope方法,首先从Engine回调Framework的入口开始。
1 | void _drawFrame() { //Engine回调Framework入口 |
核心方法 buildScope1
2
3void buildScope(Element context, [VoidCallback callback]){
...
}
需要传入一个Element的参数,这个方法通过字面意思应该理解就是对这个Element以下范围rebuild
1 | void buildScope(Element context, [VoidCallback callback]) { |
这里对上面方法做下解释
- 第1步:按照Element的深度从小到大,对_dirtyElements进行排序
由于父Widget的build方法必然会触发子Widget的build,如果先build了子Widget,后面再build父Widget时,子Widget又要被build一次。所以这样排序之后,可以避免子Widget的重复build。
- 第2步:遍历执行_dirtyElements当中element的rebuild方法
值得一提的是,遍历执行的过程中,也有可能会有新的element被加入到_dirtyElements集合中,此时会根据dirtyElements集合的长度判断是否有新的元素进来了,如果有,就重新排序。
element的rebuild方法最终会调用
performRebuild()
,而performRebuild()
不同的Element有不同的实现
- 第3步:遍历结束之后,清空dirtyElements集合
因此setState()过程主要工作是记录所有的脏元素,添加到BuildOwner对象的_dirtyElements成员变量,然后调用scheduleFrame来注册Vsync回调。 当下一次vsync信号的到来时会执行handleBeginFrame()和handleDrawFrame()来更新UI。
Element的Diff
在上面的第二步会遍历执行element的build方法
_dirtyElements[index].rebuild(); //2.遍历rebuild
element的rebuild方法最终会调用performRebuild()
,而performRebuild()
不同的Element有不同的实现,以下面两个为例
- ComponentElement,是StatefulWidget和StatelessElement的父类
- RenderObjectElement, 是有渲染功能的Element的父类
ComponentElement的performRebuild()
1
2
3
4
5
6
7
8
9
10
11void performRebuild() {
Widget built;
try {
built = build();
}
...
try {
_child = updateChild(_child, built, slot);
}
...
}
执行element的build();
,以StatefulElement的build方法为例:Widget build() => state.build(this);
。 就是执行了我们复写的StatefulWidget的state的build方法,此时创建出来的当然就是这个StatefulWidget的子Widget了
下面看下核心方法 Element updateChild(Element child, Widget newWidget, dynamic newSlot)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
//1
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
if (child != null) {
//2
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
//3
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
return child;
}
deactivateChild(child);
}
//4
return inflateWidget(newWidget, newSlot);
}
参数child 是上一次Element挂载的child Element, newWidget 是刚刚build出来的。updateChild有四种可能的情况
- 1.如果刚build出来的widget等于null,说明这个控件被删除了,child Element可以被删除了。
- 2.如果child的widget和新build出来的一样(Widget复用了),就看下位置一样不,不一样就更新下,一样就直接return了。Element还是旧的Element
- 3.看下Widget是否可以update,
Widget.canUpdate
的逻辑是判断key值和运行时类型是否相等。如果满足条件的话,就更新,并返回。
中间商的差价哪来的呢?只要新build出来的Widget和上一次的类型和Key值相同,Element就会被复用!由此也就保证了虽然Widget在不停的新建,但只要不发生大的变化,那Element是相对稳定的,也就保证了RenderObject是稳定的!
- 4.如果上述三个条件都没有满足的话,就调用
inflateWidget()
创建新的Element
这里再看下inflateWidget()
方法:
1 | Element inflateWidget(Widget newWidget, dynamic newSlot) { |
首先会尝试通过GlobalKey去查找可复用的Element,复用失败就调用Widget的方法创建新的Element,然后调用mount方法,将自己挂载到父Element上去,mount之前我们也讲过,会在这个方法里创建新的RenderObject。
RenderObjectElement的performRebuild()
1 |
|
与ComponentElement的不同之处在于,没有去build,而是调用了updateRenderObject
方法更新RenderObject。到这里我们基本就明白了Element是如何在中间应对Widget的多变,保障RenderObject的相对不变了